/*   Copyright (C) 2011 Wolfgang Hankeln, Norma J. Wendel
 *
 *   This file is part of CDinFusion.
 *
 *   CDinFusion is free software: you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public 
 *   License in version 3 (LGPL3) as published by the Free Software
 *   Foundation version 3 of the License.
 *
 *   CDinFusion is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   Lesser General Public License for more details.
 *
 *   You should have received a copy of the Lesser General Public
 *   License along with CDinFusion; see the file COPYING. If not,
 *   write to the Free Software Foundation, 59 Temple Place - Suite 330,
 *   Boston, MA 02111-1307, USA or see <http://www.gnu.org/licenses/>.
 *   
 *   If you have any questions, send an email to: megx@mpi-bremen.de.
 */
package org.cdinfusion.data.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

/**
 * Simple cache implementation that stores the information from GSC web service
 * for further use in this application. Currently the cache stores only the
 * relation between GSC short and long names.
 * <p>
 * The cache is an in memory cache but allows to create a backup on disk. This
 * backup is automatically reloaded whenever this class is loaded. If no backup
 * exists when this class is loaded, the cache will be empty.
 */
public final class GSCCache {

	/**
	 * The directory the cache files will be stored in.
	 */
	private static final File CACHE_DIR = new File(System.getenv("CATALINA_HOME") + "/webapps/CDinFusion/cache/gsc/");

	/**
	 * The map containing the in memory cache.
	 */
	private static final Map<Class<?>, String[][]> NAME_INFO_CACHE = Collections
			.synchronizedMap(new HashMap<Class<?>, String[][]>());

	static {
		// reload the cache from disk when this class is loaded
		try {
			reload();
		} catch (final IOException e) {
			System.out.println("Could not reload Cache: " + e);
			e.printStackTrace();
		}
		System.out.println("Cache size: " + NAME_INFO_CACHE.size());
	}

	/**
	 * Tests if the cache contains a value for the given key.
	 * 
	 * @param key
	 *            the key to test.
	 * @return <code>true</code> if the cache contains a value for the given
	 *         key, <code>false</code> otherwise.
	 */
	public static boolean containsKey(final Class<?> key) {
		return NAME_INFO_CACHE.containsKey(key);
	}

	/**
	 * Gets the value for the given key from the cache.
	 * 
	 * @param key
	 *            the key to get the value for.
	 * @return the value or <code>null</code> if no value for the key is stored.
	 */
	public static String[][] get(final Class<?> key) {
		return NAME_INFO_CACHE.get(key);
	}

	/**
	 * Stores the given value for the given key in the cache. If the value is
	 * <code>null</code> the value will not be stored in the cache, but a
	 * previously existing entry will be removed.
	 * 
	 * @param key
	 *            to store the value for.
	 * @param value
	 *            the value to store or <code>null</code>.
	 */
	public static void put(final Class<?> key, final String[][] value) {
		if (value == null) {
			NAME_INFO_CACHE.remove(key);
		} else {
			NAME_INFO_CACHE.put(key, value);
		}
	}

	/**
	 * Clears the in memory cache. The files on the disk are not deleted. If
	 * {@link #persist()} {@link #deletePersistentData()} are not called the old
	 * values will be reloaded to the cache when this class is loaded again.
	 */
	public static void clear() {
		NAME_INFO_CACHE.clear();
	}

	/**
	 * Reloads the cache from the file system. If the necessary files do not
	 * exist, the cache will be empty afterwards.
	 * 
	 * @throws IOException
	 *             when an IO error occurs while reloading.
	 */
	public static synchronized void reload() throws IOException {
		// clear cache to make sure that only the data from the files is in the
		// cache.
		clear();

		if (CACHE_DIR.exists()) {
			// load cache.properties -> contains information which data for
			// which class is stored in which file
			final Properties cacheProperites = loadProperties("cache.properties");

			for (final Entry<Object, Object> entry : cacheProperites.entrySet()) {
				final String className = entry.getKey().toString();
				final String propertyFileName = entry.getValue().toString();

				final Properties entryProperties = loadProperties(propertyFileName);
				final String[][] cacheValue = getCacheValueFromProperties(entryProperties);

				try {
					NAME_INFO_CACHE.put(Class.forName(className), cacheValue);
				} catch (final ClassNotFoundException e) {
					System.out.printf("Cache error: trying to reload data for "
							+ "class %s, but the class does not exist. "
							+ "No data for this class loaded.", className);
				}
			}
		}
	}

	/**
	 * Loads the specified properties file from the {@link #CACHE_DIR}.
	 * 
	 * @param fileName
	 *            the name of the properties file to load.
	 * @return the properties stored in the file.
	 * @throws IOException
	 *             if an IO error occurs
	 */
	private static Properties loadProperties(final String fileName)
			throws IOException {

		final Properties props = new Properties();
		final File propsFile = new File(CACHE_DIR, fileName);

		FileInputStream in = null;

		try {
			if (propsFile.exists()) {
				in = new FileInputStream(propsFile);
				props.load(in);
			}
		} catch (final FileNotFoundException e) {
			System.out.printf(
					"Cache error: could not load properties file %s %n.",
					propsFile.getAbsolutePath());
		} finally {
			IOUtils.closeQuietly(in);
		}

		return props;
	}

	/**
	 * Converts the properties object to the data object stored in the cache.
	 * 
	 * @param properties
	 *            the properties to convert.
	 * @return the object to be stored in the cache.
	 */
	private static String[][] getCacheValueFromProperties(
			final Properties properties) {

		final String[][] result = new String[properties.size()][2];
		int currentIndex = 0;

		for (final Entry<Object, Object> entry : properties.entrySet()) {
			result[currentIndex][0] = entry.getKey().toString();
			result[currentIndex][1] = entry.getValue().toString();

			currentIndex++;
		}

		Arrays.sort(result, new SingleIndexArrayComparator<String>(1));

		return result;
	}

	/**
	 * Writes the cache to disk.
	 * 
	 * @throws IOException
	 *             if an IO error occurs.
	 */
	public static synchronized void persist() throws IOException {
		// delete old data
		deletePersistentData();

		final Properties cacheProperties = new Properties();

		for (final Entry<Class<?>, String[][]> entry : NAME_INFO_CACHE
				.entrySet()) {

			final String className = entry.getKey().getName();
			final String fileName = className + ".properties";
			// create link from class name to file name in the cache properties
			cacheProperties.put(className, fileName);

			// convert cache value to properties
			final Properties valueProperties = getPropertiesFromCacheValue(entry
					.getValue());
			// and write them to disk
			saveProperties(valueProperties, fileName);
		}

		saveProperties(cacheProperties, "cache.properties");
	}

	/**
	 * Saves the specified properties file in the {@link #CACHE_DIR}.
	 * 
	 * @param properties
	 *            the properties to store.
	 * @param fileName
	 *            the name of the file to store the properties in.
	 */
	private static void saveProperties(final Properties properties,
			final String fileName) {

		final File propsFile = new File(CACHE_DIR, fileName);
		FileOutputStream out = null;
		final String creationMessage = String.format(
				"Created by GSCCache on %s",
				DateFormat
						.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
						.format(new Date()));
		try {
			out = new FileOutputStream(propsFile);
			properties.store(out, creationMessage);
		} catch (final IOException e) {
			System.out.printf("Cache error: IO error while writing "
					+ "properties to file %s.%n", propsFile.getAbsolutePath());
		} finally {
			IOUtils.closeQuietly(out);
		}
	}

	/**
	 * Converts a cache object to a properties object.
	 * 
	 * @param value
	 *            the cache object to convert.
	 * @return the properties object created from the cache object.
	 */
	private static Properties getPropertiesFromCacheValue(final String[][] value) {

		final Properties properties = new Properties();

		for (int i = 0; i < value.length; i++) {
			properties.put(value[i][0], value[i][1]);
		}

		return properties;
	}

	/**
	 * Deletes the cache on disk.
	 * 
	 * @throws IOException
	 *             if an IO error occurs.
	 */
	public static synchronized void deletePersistentData() throws IOException {
		FileUtils.deleteDirectory(CACHE_DIR);
		CACHE_DIR.mkdirs();
	}
}
